package org.dingfugui.triple.proxy.javassist;

import javassist.*;
import org.dingfugui.triple.proxy.ProxyFactory;
import org.dingfugui.triple.proxy.ProxyHandler;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * Created by fugui.ding on 2015/6/10.
 */
public class JavassistProxyFactory implements ProxyFactory {

    @Override
    public <T> T getProxy(Class<T> targetClass, ProxyHandler handler) {
        CtClass.debugDump = "./dump";
        String classFullName = targetClass.getName();
        ClassPool pool = ClassPool.getDefault();

        pool.importPackage(ProxyHandler.class.getName());

        CtClass c = pool.makeClass(classFullName + "$Impl");
        try {
            CtClass targetInterface = pool.get(classFullName);
            c.addInterface(targetInterface);

            CtField handlerField = CtField.make("ProxyHandler handler;", c);
            handlerField.setModifiers(Modifier.PRIVATE);
            c.addField(handlerField);

            CtField param = CtField.make("java.lang.Class targetType;", c);
            param.setModifiers(Modifier.PRIVATE);
            c.addField(param);

            Method[] targetMethods = targetClass.getMethods();

            for (Method targetMethod : targetMethods) {
                CtMethod newMethod = this.getCtMethod(targetClass, c, targetMethod, handler);
                c.addMethod(newMethod);
            }
            Class<?> cls = c.toClass();
            Object instance = cls.newInstance();

            Field f = cls.getDeclaredField("handler");
            f.setAccessible(true);
            f.set(instance, handler);

            Field f2 = cls.getDeclaredField("targetType");
            f2.setAccessible(true);
            f2.set(instance, targetClass);

            return (T) instance;
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }

    private CtMethod getCtMethod(Class<?> ti, CtClass c, Method tm, ProxyHandler handler) {
        ClassPool pool = ClassPool.getDefault();
        CtMethod newMethod = null;
        int modifiers = tm.getModifiers();
        try {
            String mname = tm.getName();
            CtClass returnType = pool.get(tm.getReturnType().getName());
            Class<?>[] params = tm.getParameterTypes();
            Class<?>[] exceptions = tm.getExceptionTypes();

            CtClass[] cParams = toCtClassArgs(params);
            CtClass[] cExceptions = toCtClassArgs(exceptions);

            String body = getMethodBody(ti, tm);

            newMethod = CtNewMethod.make(modifiers, returnType, mname, cParams, cExceptions, body, c);
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }

        return newMethod;
    }

    private String getMethodBody(Class<?> ti, Method tm) {
        StringBuilder sb = new StringBuilder();
        //String tiname = ti.getName();
        String tmname = tm.getName();
        String retype = tm.getReturnType().getSimpleName();

        sb.append("{");
        //sb.append("Class<?> ttype =\"").append(ti).append("\";");
        sb.append("String tmname=\"").append(tmname).append("\";");
        //sb.append("String retype=\"").append(retype).append("\";");
        sb.append("Class[] types=$sig;");
        sb.append("Object[] params=$args;");
        sb.append("Object result = handler.invoke(targetType,tmname,types,params);");

        if (!"void".equals(retype)) {
            sb.append("return ($r)result;");
        }

        sb.append("}");
        return sb.toString();
    }

    private CtClass[] toCtClassArgs(Class<?>[] params) {
        CtClass[] cParams = null;
        ClassPool pool = ClassPool.getDefault();
        String[] paramNames = new String[params.length];
        for (int j = 0; j < paramNames.length; j++) {
            paramNames[j] = params[j].getName();
        }
        try {
            cParams = pool.get(paramNames);
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return cParams;
    }
}
